﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Collections;
using System.Xml.Serialization;
using System.Xml;
using System.IO;
using CRMAdminDashboard.CrmSvc;

namespace CRMAdminDashboard
{
    /// <summary>
    /// Wrapper for CrmSvc.role.
    /// </summary>
    public class SystemRole
    {

        #region Fields

        /// <summary>
        /// Privileges associated to this role.
        /// </summary>
        private Dictionary<string, RolePrivilege> _rolePrivileges = new Dictionary<string, RolePrivilege>();

        /// <summary>
        /// All roles that inherit from this role, including self
        /// </summary>
        private Dictionary<Guid, SystemRole> _derivedRoles = new Dictionary<Guid, SystemRole>();

        #endregion

        #region Properties

        /// <summary>
        /// Roles's Guid in CRM.
        /// </summary>
        public Guid Id { get; set; }

        /// <summary>
        /// Parent Role's Guid in CRM, or null if the role has no parent.
        /// </summary>
        public Guid? ParentRoleId { get; set; }

        /// <summary>
        /// Roles's Business Unit Id in CRM.
        /// </summary>
        public Guid BusinessUnitId { get; set; }

        /// <summary>
        /// Roles's Business Unit Name in CRM.
        /// </summary>
        public string BusinessUnitName { get; set; }

        /// <summary>
        /// Parent Role, or null if the role has no parent.
        /// </summary>
        public SystemRole ParentRole { get; set; }

        /// <summary>
        /// Roles's Name in CRM.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Root Role's Guid in CRM. Or this role's ID if this is a root role.
        /// </summary>
        public Guid RootRoleId { get; set; }

        /// <summary>
        /// Root Role of this role, or a pointer to self if this is a root role.
        /// </summary>
        public SystemRole RootRole { get; set; }

        /// <summary>
        /// The actual CRM role
        /// </summary>
        public role InnerRole { get; set; }

        /// <summary>
        /// Minimum set of attributes to query.
        /// </summary>
        static public ColumnSet SystemRoleColumnSet { get; set; }

        /// <summary>
        /// Privileges associated to this role.
        /// </summary>
        public Dictionary<string, RolePrivilege> RolePrivileges
        {
            get
            {
                return _rolePrivileges;
            }
            set
            {
                _rolePrivileges = value;
            }
        }

        /// <summary>
        /// All roles that inherit from this role, including self
        /// </summary>
        public Dictionary<Guid, SystemRole> DerivedRoles
        {
            get
            {
                return _derivedRoles;
            }
            set
            {
                _derivedRoles = value;
            }
        }

        /// <summary>
        /// IDs of all the business units of roles that inherit from this role, including self
        /// </summary>
        public HashSet<Guid> DerivedBUs { get; set; }

        #endregion

        #region Entry Points

        /// <summary>
        /// Create a SystemRole and map all privileges.
        /// </summary>
        /// <param name="allPrivilegesByGuid">Contains all system privileges (SystemPrivilege), keyed by Guid</param>
        public SystemRole(role crmRole, CachingCrmService svcHandle, Dictionary<Guid, SystemPrivilege> allPrivilegesByGuid)
            : this(crmRole, svcHandle)
        {
            // retrieve all privileges for the role
            RetrieveRolePrivilegesRoleRequest retrieve = new RetrieveRolePrivilegesRoleRequest();
            retrieve.RoleId = this.Id;
            RetrieveRolePrivilegesRoleResponse retrieved = (RetrieveRolePrivilegesRoleResponse)svcHandle.Execute(retrieve);

            foreach (RolePrivilege rolepriv in retrieved.RolePrivileges)
            {
                // find the SystemPrivilege in the allPrivilegesByGuid list
                SystemPrivilege sysPriv = allPrivilegesByGuid[rolepriv.PrivilegeId];

                // add to the list
                this.RolePrivileges.Add(sysPriv.PrivilegeName, rolepriv);
            }
        }

        /// <summary>
        /// Create a SystemRole.
        /// </summary>
        public SystemRole(role crmRole, CachingCrmService svcHandle)
        {
            this.InnerRole = crmRole;
            this.Name = crmRole.name;
            this.BusinessUnitName = crmRole.businessunitid.name;
            this.BusinessUnitId = crmRole.businessunitid.Value;
            this.Id = crmRole.roleid.Value;
            this.ParentRoleId = crmRole.parentroleid == null ? (Guid?)null : crmRole.parentroleid.Value;
        }

        /// <summary>
        /// This will make the list box entry look nice
        /// </summary>
        public override string ToString()
        {
            return "[" + this.BusinessUnitName + "] " + this.Name;
        }

        /// <summary>
        /// Compare two sets of roles and return a RoleDiffCollection specifying the differences.
        /// </summary>
        /// <param name="allPrivilegesByGuid">Contains all system privileges (SystemPrivilege), keyed by Guid</param>
        /// <param name="leftList">A list of SystemRoles representing the left set</param>
        /// <param name="rightList">A list of SystemRoles representing the right set</param>
        public static RoleDiffCollection Compare(CachingCrmService svcHandle, Dictionary<Guid, SystemPrivilege> allPrivilegesByGuid,
            IEnumerable<SystemRole> leftList, IEnumerable<SystemRole> rightList)
        {
            // Merge the two sets
            SystemRole leftMerged = SystemRole.MergeRoles(svcHandle, allPrivilegesByGuid, leftList);
            SystemRole rightMerged = SystemRole.MergeRoles(svcHandle, allPrivilegesByGuid, rightList);

            // we create a copy of the right privilege set
            Hashtable rightRolePrivilegesCopy = new Hashtable(rightMerged.RolePrivileges);

            RoleDiffCollection differences = new RoleDiffCollection(leftList, rightList);
            foreach (string privName in leftMerged.RolePrivileges.Keys)
            {
                RolePrivilege leftPriv = (RolePrivilege)leftMerged.RolePrivileges[privName];
                if (rightMerged.RolePrivileges.ContainsKey(privName))
                {
                    // privName is in both sets, check that it has the same depth
                    RolePrivilege rightPriv = (RolePrivilege)rightMerged.RolePrivileges[privName];
                    if (leftPriv.Depth != rightPriv.Depth)
                    {
                        // the privilege has different depths, i.e. we need it in the diff list.
                        differences.RoleDifferences.Add(new RoleDiff(privName, allPrivilegesByGuid, leftPriv, rightPriv));
                    }

                    // remove the privilege from the right copy
                    rightRolePrivilegesCopy.Remove(privName);
                }
                else
                {
                    // privName is only in the left set: create an entry in the diff list
                    differences.RoleDifferences.Add(new RoleDiff(privName, allPrivilegesByGuid, leftPriv, null));
                }
            }

            // see which privileges are left in the copy and add them to the diff list
            foreach (string privName in rightRolePrivilegesCopy.Keys)
            {
                RolePrivilege rightPriv = (RolePrivilege)rightMerged.RolePrivileges[privName];
                differences.RoleDifferences.Add(new RoleDiff(privName, allPrivilegesByGuid, null, rightPriv));
            }

            return differences;
        }

        /// <summary>
        /// Return a RoleDiffCollection specifying the merged privileges. This is equivalent to the other Compare
        /// overload with the difference that the second set is empty.
        /// </summary>
        public static RoleDiffCollection Compare(CachingCrmService svcHandle, Dictionary<Guid, SystemPrivilege> allPrivilegesByGuid,
            IEnumerable<SystemRole> list)
        {
            // Merge the set
            SystemRole merged = SystemRole.MergeRoles(svcHandle, allPrivilegesByGuid, list);

            RoleDiffCollection differences = new RoleDiffCollection(list);
            foreach (string privName in merged.RolePrivileges.Keys)
            {
                RolePrivilege rolePriv = (RolePrivilege)merged.RolePrivileges[privName];
                differences.RoleDifferences.Add(new RoleDiff(privName, allPrivilegesByGuid, rolePriv));
            }

            return differences;
        }

        /// <summary>
        /// Create role equivalent to the roles selected in list.
        /// </summary>
        /// <param name="roleName">New role name</param>
        /// <param name="bu">new role Business Unit</param>
        public static void CreateEquivalentRole(CachingCrmService svcHandle, string roleName, BusinessUnit bu,
            Dictionary<string, SystemPrivilege> allPrivilegesByName,
            Dictionary<Guid, SystemPrivilege> allPrivilegesByGuid,
            IEnumerable<SystemRole> list)
        {
            role newRole = CreateEmptyRole(svcHandle, roleName, bu, allPrivilegesByName);
            SystemRole oldMergedSR = SystemRole.MergeRoles(svcHandle, allPrivilegesByGuid, list);
            SystemRole newMergedSR = new SystemRole(newRole, svcHandle, allPrivilegesByGuid);

            AddPrivilegesRoleRequest request = new AddPrivilegesRoleRequest();
            request.RoleId = newRole.roleid.Value;
            request.Privileges = new RolePrivilege[oldMergedSR.RolePrivileges.Count];

            int i = 0;
            foreach (RolePrivilege rp in oldMergedSR.RolePrivileges.Values)
            {
                request.Privileges[i++] = CreatePrivilege(rp.PrivilegeId, rp.Depth);
            }

            AddPrivilegesRoleResponse response = (AddPrivilegesRoleResponse)svcHandle.Execute(request);
        }

        /// <summary>
        /// Create role equivalent to the subtraction of roles (A - B)
        /// </summary>
        /// <param name="roleName">New role name</param>
        /// <param name="bu">new role Business Unit</param>
        public static void CreateSubtractionRole(CachingCrmService svcHandle, string roleName, BusinessUnit bu,
            Dictionary<string, SystemPrivilege> allPrivilegesByName,
            Dictionary<Guid, SystemPrivilege> allPrivilegesByGuid,
            IEnumerable<SystemRole> listA,
            IEnumerable<SystemRole> listB)
        {
            role newRole = CreateEmptyRole(svcHandle, roleName, bu, allPrivilegesByName);
            SystemRole newRoleSR = new SystemRole(newRole, svcHandle, allPrivilegesByGuid);
            RoleDiffCollection differences = Compare(svcHandle, allPrivilegesByGuid, listA, listB);

            AddPrivilegesRoleRequest request = new AddPrivilegesRoleRequest();
            request.RoleId = newRole.roleid.Value;

            List<RolePrivilege> privs = new List<RolePrivilege>();
            foreach (RoleDiff diff in differences.RoleDifferences)
            {
                if (diff.RightPrivilegeDepth == null)
                {
                    // this privilege is only in A, therefore it is part of C
                    privs.Add(CreatePrivilege(allPrivilegesByName[diff.PrivilegeName].Id, (PrivilegeDepth)diff.LeftPrivilegeDepth));
                }
                else if (diff.LeftPrivilegeDepth == null)
                {
                    // this privilege is only in B, therefore it is not part of C
                }
                else
                {
                    if (diff.RightPrivilegeDepth.Value == PrivilegeDepth.Global)
                    {
                        // nothing to do, B is already the highest possible
                    }
                    else if (diff.RightPrivilegeDepth.Value == PrivilegeDepth.Deep)
                    {
                        if (diff.LeftPrivilegeDepth.Value == PrivilegeDepth.Global)
                        {
                            // A has the highest depth
                            privs.Add(CreatePrivilege(allPrivilegesByName[diff.PrivilegeName].Id, (PrivilegeDepth)diff.LeftPrivilegeDepth));
                        }
                    }
                    else if (diff.RightPrivilegeDepth.Value == PrivilegeDepth.Local)
                    {
                        if (diff.LeftPrivilegeDepth.Value == PrivilegeDepth.Global ||
                            diff.LeftPrivilegeDepth.Value == PrivilegeDepth.Deep)
                        {
                            // A has the highest depth
                            privs.Add(CreatePrivilege(allPrivilegesByName[diff.PrivilegeName].Id, (PrivilegeDepth)diff.LeftPrivilegeDepth));
                        }
                    }
                    else
                    {
                        if (diff.LeftPrivilegeDepth.Value == PrivilegeDepth.Global ||
                            diff.LeftPrivilegeDepth.Value == PrivilegeDepth.Deep ||
                            diff.LeftPrivilegeDepth.Value == PrivilegeDepth.Local)
                        {
                            // A has the highest depth
                            privs.Add(CreatePrivilege(allPrivilegesByName[diff.PrivilegeName].Id, (PrivilegeDepth)diff.LeftPrivilegeDepth));
                        }
                    }
                }
            }

            request.Privileges = privs.ToArray();
            AddPrivilegesRoleResponse response = (AddPrivilegesRoleResponse)svcHandle.Execute(request);
        }

        /// <summary>
        /// Return all the users that have this security role
        /// </summary>
        public Dictionary<Guid, SystemUser> GetUsers(CachingCrmService svcHandle, ColumnSetBase columns, RoleUserHierarchy rUHierarchy)
        {
            List<systemuser> users = this.GetUsersInternal(svcHandle, columns, rUHierarchy);

            Dictionary<Guid, SystemUser> list = new Dictionary<Guid, SystemUser>();
            foreach (systemuser user in users)
            {
                list.Add(user.systemuserid.Value, new SystemUser(user));
            }

            return list;
        }

        /// <summary>
        /// Return all the users that have this security role or a role inherited
        /// </summary>
        public Dictionary<Guid, SystemUser> GetUsersDeep(CachingCrmService svcHandle, RoleUserHierarchy rUHierarchy)
        {
            // users who own this particular role
            HashSet<Guid> deepUsers = this.AddLocalUsers(svcHandle, new HashSet<Guid>(), rUHierarchy);

            // look for users who own derived roles
            foreach (SystemRole sr in this.DerivedRoles.Values)
            {
                deepUsers = sr.AddLocalUsers(svcHandle, deepUsers, rUHierarchy);
            }

            // transform them into a set of SystemUser references from the hierarchy
            Dictionary<Guid, SystemUser> result = new Dictionary<Guid, SystemUser>();
            foreach (Guid id in deepUsers)
            {
                SystemUser user = rUHierarchy.AllUsers[id];
                result.Add(user.Id, user);
            }

            return result;
        }

        /// <summary>
        /// Get all system roles. If allPrivilegesByGuid is != null, also map privileges
        /// </summary>
        public static Dictionary<Guid, SystemRole> GetAllRoles(CachingCrmService svcHandle, Dictionary<Guid,
            SystemPrivilege> allPrivilegesByGuid, ColumnSetBase columns)
        {
            // get all roles
            QueryExpression query = new QueryExpression();
            query.EntityName = EntityName.role.ToString();
            query.ColumnSet = columns;

            BusinessEntityCollection roles = svcHandle.RetrieveMultiple(query);
            Dictionary<Guid, SystemRole> list = new Dictionary<Guid, SystemRole>();
            foreach (BusinessEntity be in roles.BusinessEntities)
            {
                if (allPrivilegesByGuid != null)
                {
                    list.Add(((role)be).roleid.Value, new SystemRole((role)be, svcHandle, allPrivilegesByGuid));
                }
                else
                {
                    list.Add(((role)be).roleid.Value, new SystemRole((role)be, svcHandle));
                }
            }

            return list;
        }

        /// <summary>
        /// Return all the roles derived from this role
        /// </summary>
        public Dictionary<Guid, SystemRole> GetDerivedRoles(Dictionary<Guid, SystemRole> allRoles)
        {
            Dictionary<Guid, SystemRole> list = new Dictionary<Guid, SystemRole>();
            foreach (SystemRole sr in allRoles.Values)
            {
                if (this.Id.Equals(sr.ParentRoleId))
                {
                    list.Add(sr.Id, sr);
                    foreach (SystemRole child in sr.GetDerivedRoles(allRoles).Values)
                    {
                        list.Add(child.Id, child);
                    }
                }
            }
            return list;
        }

        /// <summary>
        /// Search derived roles and find a role matching a particular BU
        /// </summary>
        public SystemRole FindRoleByBU(Guid BusinessUnitId)
        {
            if (this.BusinessUnitId.Equals(BusinessUnitId))
            {
                return this;
            }
            else
            {
                foreach (SystemRole derived in this.DerivedRoles.Values)
                {
                    if (derived.BusinessUnitId.Equals(BusinessUnitId))
                    {
                        return derived;
                    }
                }
            }

            return null;
        }

        #endregion

        #region Local Methods

        /// <summary>
        /// Static initializer
        /// </summary>
        static SystemRole()
        {
            SystemRoleColumnSet = new ColumnSet();
            SystemRoleColumnSet.Attributes = new string[] { "businessunitid", "name", "parentroleid" };
        }

        /// <summary>
        /// Inernal constructor, used while merging.
        /// </summary>
        private SystemRole() { }

        /// <summary>
        /// Return all the users that have this security role as a list of <systemuser>
        /// </summary>
        private List<systemuser> GetUsersInternal(CachingCrmService svcHandle, ColumnSetBase columns, RoleUserHierarchy rUHierarchy)
        {
            ConditionExpression condition = new ConditionExpression();
            condition.AttributeName = "roleid";
            condition.Operator = ConditionOperator.Equal;
            condition.Values = new object[] { this.Id };

            LinkEntity link = new LinkEntity();
            link.LinkFromEntityName = EntityName.systemuser.ToString();
            link.LinkFromAttributeName = "systemuserid";
            link.LinkToEntityName = "systemuserroles";
            link.LinkToAttributeName = "systemuserid";
            link.LinkCriteria = new FilterExpression();
            link.LinkCriteria.Conditions = new ConditionExpression[] { condition };

            QueryExpression query = new QueryExpression();
            query.EntityName = EntityName.systemuser.ToString();
            query.LinkEntities = new LinkEntity[] { link };
            query.ColumnSet = columns;

            RetrieveMultipleRequest retrieveRequest = new RetrieveMultipleRequest();
            retrieveRequest.Query = query;
            RetrieveMultipleResponse retrieveResponse = (RetrieveMultipleResponse)svcHandle.Execute(retrieveRequest);

            List<systemuser> list = new List<systemuser>();
            foreach (BusinessEntity be in retrieveResponse.BusinessEntityCollection.BusinessEntities)
            {
                // we only add users that are in the All Users list (that one has the right knowledge about who to include)
                if (rUHierarchy.AllUsers.ContainsKey(((systemuser)be).systemuserid.Value))
                {
                    list.Add((systemuser)be);
                }
            }

            return list;
        }

        /// <summary>
        /// Return all the users that have this security role and merged them with the input set (allUsers)
        /// </summary>
        private HashSet<Guid> AddLocalUsers(CachingCrmService svcHandle, HashSet<Guid> allUsers, RoleUserHierarchy rUHierarchy)
        {
            // users who own this particular role
            List<systemuser> localUsers = this.GetUsersInternal(svcHandle, SystemUser.SystemUserColumnSet, rUHierarchy);
            foreach (systemuser localUser in localUsers)
            {
                if (!allUsers.Contains(localUser.systemuserid.Value))
                {
                    allUsers.Add(localUser.systemuserid.Value);
                }
            }

            return allUsers;
        }

        #endregion

        #region Utilities

        /// <summary>
        /// Merge a list of SystemRoles so that the privilege set is an union of all privileges. Much as CRM does it.
        /// </summary>
        /// <param name="allPrivilegesByGuid">Contains all system privileges (SystemPrivilege), keyed by Guid</param>
        /// <param name="lstSystemRoles">A list of SystemRoles</param>
        private static SystemRole MergeRoles(CachingCrmService svcHandle, Dictionary<Guid, SystemPrivilege> allPrivilegesByGuid,
            IEnumerable<SystemRole> lstSystemRoles)
        {
            SystemRole merged = new SystemRole();

            foreach (SystemRole role in lstSystemRoles)
            {
                // we get the role again so we can keep comparing while modifying the role (i.e. the roles are not cached)
                role freshRole = (role)svcHandle.RetrieveNoCache(EntityName.role.ToString(), role.Id, SystemRole.SystemRoleColumnSet);
                SystemRole freshSystemRole = new SystemRole(freshRole, svcHandle, allPrivilegesByGuid);

                merged.Merge(freshSystemRole);
            }

            return merged;
        }

        /// <summary>
        /// Merge with anoter role.
        /// </summary>
        private void Merge(SystemRole role)
        {
            // we merge the privileges two ways, to make sure that if the two sets have a non null XOR we account for everything
            Dictionary<string, RolePrivilege> newRolePrivileges = new Dictionary<string, RolePrivilege>();
            foreach (string privilegeName in this.RolePrivileges.Keys)
            {
                if (role.RolePrivileges.ContainsKey(privilegeName))
                {
                    // the privilege is in both roles: merge
                    newRolePrivileges[privilegeName] = MergePrivilege(
                        (RolePrivilege)this.RolePrivileges[privilegeName],
                        (RolePrivilege)role.RolePrivileges[privilegeName]);
                }
                else
                {
                    // the privilege is only in this role
                    newRolePrivileges[privilegeName] = (RolePrivilege)this.RolePrivileges[privilegeName];
                }
            }

            // look for privileges that are in the other role but not in this
            foreach (string privilegeName in role.RolePrivileges.Keys)
            {
                if (!newRolePrivileges.ContainsKey(privilegeName))
                {
                    newRolePrivileges[privilegeName] = (RolePrivilege)role.RolePrivileges[privilegeName];
                }
            }

            this.RolePrivileges = newRolePrivileges;

            // we reset all the other properties
            this.Id = Guid.Empty;
            this.ParentRoleId = null;
            this.Name = null;
            this.BusinessUnitName = null;
            this.BusinessUnitId = Guid.Empty;
            this.DerivedRoles.Clear();
        }

        /// <summary>
        /// Returns the deepest depth between two role privileges.
        /// </summary>
        private static RolePrivilege MergePrivilege(RolePrivilege rp1, RolePrivilege rp2)
        {
            if (rp1.Depth == PrivilegeDepth.Global)
            {
                return rp1;
            }
            else if (rp2.Depth == PrivilegeDepth.Global)
            {
                return rp2;
            }
            else if (rp1.Depth == PrivilegeDepth.Deep)
            {
                return rp1;
            }
            else if (rp2.Depth == PrivilegeDepth.Deep)
            {
                return rp2;
            }
            else if (rp1.Depth == PrivilegeDepth.Local)
            {
                return rp1;
            }
            else if (rp2.Depth == PrivilegeDepth.Local)
            {
                return rp2;
            }

            return rp1;
        }

        /// <summary>
        /// Create a role in CRM
        /// </summary>
        /// <param name="allPrivilegesByName">Contains all system privileges (SystemPrivilege), keyed by Name</param>
        static private role CreateEmptyRole(CachingCrmService svcHandle, string roleName, BusinessUnit bu, Dictionary<string, SystemPrivilege> allPrivilegesByName)
        {
            role newRole = new role();
            newRole.name = roleName;
            newRole.businessunitid = new Lookup();
            newRole.businessunitid.type = EntityName.businessunit.ToString();
            newRole.businessunitid.name = bu.Name;
            newRole.businessunitid.Value = bu.Id;

            Guid newRoleGuid = svcHandle.Create(newRole);
            svcHandle.InvalidateCache();
            newRole.roleid = new Key();
            newRole.roleid.Value = newRoleGuid;

            return newRole;
        }

        /// <summary>
        /// Create a privilege for AddPrivilegesRoleResponse 
        /// </summary>
        private static RolePrivilege CreatePrivilege(Guid privId, PrivilegeDepth privilegeDepth)
        {
            RolePrivilege prv = new RolePrivilege();

            prv.Depth = privilegeDepth;
            prv.PrivilegeId = privId;

            return prv;
        }

        #endregion

    }
}
